const std = @import("std");

const Vector = @import("vector.zig").Vector;

const ASCII_INT_OFFSET = 48;
const GRID_START_LINE = 1;

const ReadError = error{
	ParseError,
};


pub fn LevelReader(comptime CharType: type) type {
	return struct {
		allocator: std.mem.Allocator = std.heap.page_allocator,
		_buffer: []CharType = undefined,
		_grid_size: Vector = undefined,
		_char_map: std.AutoHashMap(CharType, std.ArrayList(Vector)) = undefined,

		const Self = @This();


		pub fn init(self: *Self, filename: []const u8) !void {
			self._char_map = std.AutoHashMap(CharType, std.ArrayList(Vector))
					.init(self.allocator);

			try self.read_file(filename);

			self._grid_size = try self.parse_grid_size();
			try self.parse_grid();
		}


		pub fn deinit(self: *Self) void {
			self.allocator.free(self._buffer);

			var key_iter = self._char_map.keyIterator();
			while (key_iter.next()) |key_ptr| {
				const key = key_ptr.*;
				self._char_map.get(key).?.deinit();
			}
			self._char_map.deinit();
		}


		pub fn get_positions(self: *const Self, char: CharType)
				?std.ArrayList(Vector) {
			return self._char_map.get(char);
		}


		pub fn get_grid_size(self: *Self) Vector {
			return self._grid_size;
		}


		fn read_file(self: *Self, filename: []const u8) !void {
			var file = try std.fs.cwd().openFile(filename, .{});
			defer file.close();

			const u8_buffer: []u8 = try file.reader()
				.readAllAlloc(self.allocator, 256*256);
			defer self.allocator.free(u8_buffer);

			self._buffer = try self.allocator.alloc(CharType, u8_buffer.len);
			var char_count: usize = 0;
			var utf8_iterator = (try std.unicode.Utf8View.init(u8_buffer)).iterator();
			while (utf8_iterator.nextCodepoint()) |code_point| {
				self._buffer[char_count] = @truncate(code_point);
				char_count += 1;
			}

			// Resize because the number of code points may not equal the number
			// of bytes
			if (self.allocator.resize(self._buffer, char_count)) {
				self._buffer = self._buffer[0..char_count];
			} else {
				return error.OutOfMemory;
			}
		}


		fn parse_grid_size(self: *Self) !Vector {
			const grid_size_line = self.get_buffer_line(0)
					orelse return ReadError.ParseError;

			var dimensions_iter = std.mem.splitScalar(
					CharType, grid_size_line, ',');

			const width_str: []const CharType = dimensions_iter.next()
				orelse return ReadError.ParseError;
			const height_str: []const CharType = dimensions_iter.next()
				orelse return ReadError.ParseError;

			const width: u8 = std.fmt.parseIntWithGenericCharacter(
				u8,
				CharType,
				width_str,
				10
			) catch return ReadError.ParseError;
			const height: u8 = std.fmt.parseIntWithGenericCharacter(
				u8,
				CharType,
				height_str,
				10
			) catch return ReadError.ParseError;

			return Vector{.x = width, .y = height};
		}


		fn parse_grid(self: *Self) !void {
			var grid_start_idx: usize = 0;
			for (0..GRID_START_LINE) |_| {
				grid_start_idx = std.mem.indexOfScalarPos(
					CharType,
					self._buffer,
					grid_start_idx,
					'\n'
				) orelse {
					return ReadError.ParseError;
				};
				grid_start_idx += 1;
			}

			var x: u8 = 0;
			var y: u8 = 0;
			for (grid_start_idx..self._buffer.len) |idx| {
				const char = self._buffer[idx];
				if (char == '\n') {
					y +|= 1;
					x = 0;
				} else {
					var new_char: bool = false;
					var opt_list_ptr: ?*std.ArrayList(Vector)
							= self._char_map.getPtr(char);
					if (opt_list_ptr == null) {
						new_char = true;
						const list = try std.ArrayList(Vector)
								.initCapacity(self.allocator, 10);
						try self._char_map.put(char, list);
						opt_list_ptr = self._char_map.getPtr(char);
					}

					const list_ptr = opt_list_ptr.?;
					try list_ptr.*.append(Vector{.x = x, .y = y});
					x +|= 1;
				}

				if (x > self._grid_size.x or y > self._grid_size.y) {
					return ReadError.ParseError;
				}
			}
		}


		fn get_buffer_line(self: *Self, line: u8) ?[]const CharType {
			var line_iter = std.mem.splitScalar(CharType, self._buffer, '\n');
			var line_slice: ?[]const CharType = undefined;
			for (0..line + 1) |_| {
				line_slice = line_iter.next();
			}

			return line_slice;
		}
	};
}
